/*
 * Copyright (c) Ulrich Hilger, http://dev.uhilger.de, all rights reserved.
 *
 * Published under the terms and conditions of the Apache License 2.0,
 * see http://www.apache.org/licenses/LICENSE-2.0
 */

package de.uhilger.servicehub.server;

import de.uhilger.servicehub.model.JDBCUserStore;
import de.uhilger.servicehub.model.ServletContainer;
import de.uhilger.servicehub.model.UserStore;
import java.io.File;
import java.net.InetAddress;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import org.apache.catalina.Container;
import org.apache.catalina.Context;
import org.apache.catalina.Engine;
import org.apache.catalina.Host;
import org.apache.catalina.authenticator.SingleSignOn;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.StandardHost;
import org.apache.catalina.realm.JDBCRealm;
import org.apache.catalina.startup.Embedded;
import org.apache.catalina.startup.ExpandWar;
import org.apache.catalina.util.ServerInfo;

/**
 * <p>An embedded Tomcat server</p>
 *
 * @author Ulrich Hilger, <a href="http://dev.uhilger.de" target="_blank">http://dev.uhilger.de</a>
 * @author Published under the terms and conditions of
 * the <a href="http://www.apache.org/licenses/LICENSE-2.0" target="_blank">Apache License 2.0</a>
 */
public class EmbeddedTomcat implements ServletContainer {

/*
 * <p>Basic example came from the article "Embedding Tomcat Into Java Applications"<br>
 * <a href="http://onjava.com/pub/a/onjava/2002/04/03/tomcat.html" target="_blank">http://onjava.com/pub/a/onjava/2002/04/03/tomcat.html</a><br>
 * of April 3, 2002 by James Goodwill.</p>
 */

  /**
   * name of this server
   */
  private static final String SERVER_NAME = "Catalina";

  private String basePath;
  private String homePath;
  private Embedded embeddedServer;
  private Host localhost;
  private Connector httpConnector;
  private JDBCRealm realm;
  private UserStore userStore;
  private String storageName;
  private String user;
  private String password;
  private boolean isStarted = false;

  private Engine engine;

  /**
   * Create a new object of class EmbeddedTomcat
   */
  public EmbeddedTomcat() {
  }

  /**
   * <p>Set base path of server instance. This is the path to a directory having
   * directories</p>
   *
   * <p>/conf<br>
   * /logs<br>
   * /webapps<br>
   * /work<br>
   * /temp</p>
   *
   * @param basePath - the path to the base directory of the server instance
   */
  public void setBase(String basePath) {
    this.basePath = basePath;
  }

  /**
   * <p>Get the path of the base directory of this server, i.e. the directory
   * having directories</p>
   *
   * <p>/conf<br>
   * /logs<br>
   * /webapps<br>
   * /work<br>
   * /temp</p>
   *
   * @return - the path of the base directory
   * @see #setBase(java.lang.String)
   */
  public String getBase() {
    return basePath;
  }

  /**
   * Get the directory where to find the Tomcat binaries
   * @return  the tomcat home directory
   */
  public String getHome() {
    return homePath;
  }

  /**
   * Set the directory where to find the Tomcat binaries
   * @param homePath  the tomcat home directory
   */
  public void setHome(String homePath) {
    this.homePath = homePath;
  }

  /**
   * Get information about the servlet container
   * @return  servlet container information
   */
  public String getInfo() {
    //return embeddedServer.getInfo();
    return ServerInfo.getServerInfo() + " (" + ServerInfo.getServerBuilt() + ")";
  }

  /**
   * Get the contexts that are currently deployed on the servlet container
   * @return  the list of deployed contexts
   */
  public String[] getApps() {
    Container[] appContexts = localhost.findChildren();
    List list = new ArrayList();
    File[] files = new File(getBase() + File.separator + ServletContainer.DEFAULT_WEBAPPS_PATH).listFiles();
    for(int i = 0; i < files.length; i++) {
      if(files[i].isDirectory()) {
        String dirName = files[i].getName();
        //System.out.println("getApps checks dir " + dirName);
        int index = getContextIndex(files[i].getName(), appContexts);
        if(index < 0) {
          list.add(dirName + " " + ServletContainer.STOPPED);
        } else {
          Context context = (Context) appContexts[index];
          list.add(context.getPath() + ": " + context.getDisplayName());
        }
      }
    }
    //Container[] appContexts = tomcat.getApps();
    /*for(int i = 0; i < appContexts.length; i++) {
      if(appContexts[i] != null && appContexts[i] instanceof Context) {
        Context context = (Context) appContexts[i];
        //ServiceHub.log("getApps adds context path " + context.getDisplayName() + " to list", getClass(), Level.FINEST);
        //ServiceHub.log("getApps context doc base " + context.getDocBase(), getClass(), Level.FINEST);
        String contextPath = context.getPath();
        if(contextPath != null && contextPath.length() >1) {
          list.add(context.getPath() + ": " + context.getDisplayName());
        } else {
          list.add(URL_SEPARATOR + ": " + context.getDisplayName());
        }
        }
    }*/
    return (String[]) list.toArray(new String[0]);
  }

  /**
   * Get a connection to this servers user database
   * @return the user database connection
   * @throws SQLException
   */
	/*private Connection getConnection() throws SQLException {
    StringBuffer connect = new StringBuffer();
    connect.append(realm.getConnectionURL());
    Connection connection = DriverManager.getConnection(connect.toString(),
      realm.getConnectionName(), realm.getConnectionPassword());
    return connection;
	}*/

  /**
   * Set the name of the user storage directory relative to this.getBase()
   * @param storageName  the relative storage name
   */
  public void setUserStorageName(String storageName) {
    this.storageName = storageName;
  }

  /**
   * <p>Start the server, i.e. its servlet container and database server.</p>
   * 
   * <p>
   * embeddedServer (Tomcat) -> engine (Catalina) -> localhost (Host) -> rootContext (Context)
   * </p>
   */
  public void start(int port) throws Exception {

    /*String configFile = System.getProperty("java.security.auth.login.config");
    System.out.println("Config file before change: " + configFile);
    //URL url = Launcher.class.getClassLoader().getResource(configFile);
    File file = new File(getBase() + "/conf/catalina.policy");

    //System.setProperty("java.security.auth.login.config", url.toExternalForm());
    System.setProperty("java.security.auth.login.config", file.toURI().toURL().toExternalForm());
    configFile = System.getProperty("java.security.auth.login.config");
    System.out.println("Config file after change: " + configFile);*/

    embeddedServer = new Embedded();
    embeddedServer.setCatalinaHome(getHome()); // where tomcat runtime lives
    embeddedServer.setCatalinaBase(getBase()); // where web apps and tomcat configuration live
    embeddedServer.setName(SERVER_NAME);
    //embeddedServer.addAuthenticator(new FormAuthenticator(), "FORM BASED");

    if(userStore != null && userStore instanceof JDBCUserStore) {
      JDBCUserStore jdbcStore = (JDBCUserStore) userStore;
      realm = new JDBCRealm();
      realm.setDriverName(jdbcStore.getJDBCDriverClassName());
      realm.setConnectionURL(jdbcStore.getConnectString() + getBase() + File.separator + storageName/* + DB_CREATE*/);
      //ServiceHub.log("startTomcat realm url: " + realm.getConnectionURL(), getClass(), Level.FINEST);
      realm.setConnectionName(user);
      realm.setConnectionPassword(password);
      realm.setUserNameCol(jdbcStore.getUserTableUserNameField());
      realm.setUserCredCol(jdbcStore.getUserPasswordField());
      realm.setRoleNameCol(jdbcStore.getRoleTableRoleNameField());
      realm.setUserTable(jdbcStore.getSchemaName() + "." + jdbcStore.getUserTableName());
      realm.setUserRoleTable(jdbcStore.getSchemaName() + "." + jdbcStore.getRoleMapTableName());
      //createDbTables();
      embeddedServer.setRealm(realm);
    }

    engine = embeddedServer.createEngine();
    engine.setName(SERVER_NAME);
    engine.setDefaultHost(LOCALHOST);

    String webappsPath = getBase() + URL_SEPARATOR + DEFAULT_WEBAPPS_PATH;
    localhost = embeddedServer.createHost(LOCALHOST, webappsPath);
    localhost.setAppBase(DEFAULT_WEBAPPS_PATH);
    if(localhost instanceof StandardHost) {
      ((StandardHost) localhost).addValve(new SingleSignOn());
    }
    engine.addChild(localhost);


    File webappsDir = new File(webappsPath);
    File[] contextDir = webappsDir.listFiles();
    if(contextDir != null) {
      for(int i = 0; i < contextDir.length; i++) {
        if(contextDir[i].isDirectory()) {
          Context context = null;
          String contextPath = URL_SEPARATOR + contextDir[i].getName();
          if(contextPath.endsWith(DEFAULT_ROOT)) {
            contextPath = "";
          }
          if(contextPath.endsWith("server")) {
            String docBase = webappsPath + URL_SEPARATOR + contextDir[i].getName() +
                    URL_SEPARATOR + "webapps" + URL_SEPARATOR + "admin";
            contextPath = URL_SEPARATOR + "admin";
            context = embeddedServer.createContext(contextPath, docBase);
            context.setPrivileged(true);
            localhost.addChild(context);
          } else {
            String docBase = webappsPath + URL_SEPARATOR + contextDir[i].getName();
            context = embeddedServer.createContext(contextPath, docBase);
            context.setPrivileged(true);
            localhost.addChild(context);
          }
        }
      }
    }

    embeddedServer.addEngine(engine);

    InetAddress address = null;
    httpConnector = embeddedServer.createConnector(address, port, false);
    embeddedServer.addConnector(httpConnector);

    embeddedServer.start();
    isStarted = true;
  }

  /**
   * Stop the server, i.e. shut down its servlet container and database server.
   */
  public void stop() throws Exception {
    embeddedServer.stop();
    isStarted = false;
  }

  /**
   * Registers a WAR with the container.
   *
   * @param contextPath -
   *            the context path under which the application will be
   *            registered
   * @param absolutePath -
   *            path to app to register
   */
  public void registerWAR(String contextPath, String absolutePath)
          throws Exception {
    //ServiceHub.log("registerWar contextPath " + contextPath +
      //      " absolutePath " + absolutePath, getClass(), Level.FINEST);
    if(!contextPath.startsWith(URL_SEPARATOR)) {
      contextPath = URL_SEPARATOR + contextPath;
    }
    Context context = embeddedServer.createContext(contextPath, absolutePath);
    context.setReloadable(false);
    localhost.addChild(context);
  }

  /**
   * Unregisters a WAR from the web server.
   *
   * @param contextPath -
   *            the context path to be removed
   */
  public void unregisterWAR(String contextPath) throws Exception {

    Context context = localhost.map(contextPath);
    if (context != null) {
      embeddedServer.removeContext(context);
    } else {
      throw new Exception("Context does not exist for named path : " + contextPath);
    }
  }


  /**
   * Registers a WAR with the container.
   *
   * @param contextPath - the context path under which the
   *               application will be registered
   * @param warFile - the URL of the WAR to be
   * registered.
   */
  public void deployWAR(String contextPath, URL warFile) throws Exception {
    /*if(localhost.map(contextPath) != null/*contextPathExists(contextPath)*//*) {
      throw new Exception("Context " + contextPath + " already exists.");
    } else {
      File file = new File(fileName);
      String urlStr = ServiceHub.JAR_PREFIX + file.toURI().toURL().toString() + "!/";
      ServiceHub.log("deploy urlStr=" + urlStr, getClass(), Level.FINEST);
      ExpandWar.expand(localhost, new URL(urlStr), contextPath);
    }*/
    ExpandWar.expand(localhost, warFile, contextPath);
    registerWAR(contextPath, getBase() + File.separator +
            DEFAULT_WEBAPPS_PATH + File.separator + contextPath);
  }

  /**
   * Determine whether or not a context is already deployed on this server
   * @param contextPath  the context path to find
   * @return  true, when context path exists, false if not
   */
  public boolean contextPathExists(String contextPath) {
    return (localhost.map(contextPath) != null);
  }
    //boolean isPresent = false;
    //Container[] container = localhost.findChildren();
    /*for(int i = 0; i < container.length && !isPresent; i++) {
      isPresent = ((Context) container[i]).getDocBase().equals(contextPath);
    }*/
    //return isPresent;
    //return getContextIndex(contextPath, localhost.findChildren()) > -1;
  //}

  private int getContextIndex(String contextPath, Container[] webapps) {
    //System.out.println("check if contextPath " + contextPath + " exists");
    int index = -1;
    for(int i = 0; i < webapps.length && index < 0; i++) {
      if(((Context) webapps[i]).getDocBase().equals(contextPath)) {
        index = i;
      }
    }
    return index;
  }

  public void setUserStore(UserStore userStore) {
    this.userStore = userStore;
    if(userStore instanceof JDBCUserStore) {
      JDBCUserStore jdbcStore = (JDBCUserStore) userStore;
  		try { Class.forName(jdbcStore.getJDBCDriverClassName()).newInstance(); }
    	catch (Exception e) { e.printStackTrace(); }
    }
  }

  /**
   * Normally this method also needs to change the user on the jdbc realm when the server runs at
   * the time this method is called but the user interface of servicehub does allow db user name
   * changes only when the server is stopped.
   * @param dbUser
   */
  public void setDbUser(String dbUser) {
    this.user = dbUser;
  }

  public void setDbPassword(String dbPass) {
    this.password = dbPass;
    // TODO: set jdbc realm to new password, when server runs at the time this method is called
    if(isStarted) {
      realm.setConnectionPassword(this.password);
      embeddedServer.setRealm(realm);
    }
  }



}
